/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2019-2019.
 * Description: hwdog driver for kernel
 * Author: xiaowei
 * Create: 2019-07-18
 */
#include <linux/of_irq.h>
#include <linux/sched/clock.h>
#include <linux/interrupt.h>
#include <linux/hrtimer.h>
#include <linux/notifier.h>
#include <linux/reboot.h>
#include <linux/hal/gpio_lock.h>
#include <linux/hal/watchdog.h>
#include <linux/hal/acpi_enable.h>
#include <linux/hal/fiq_glue.h>
#include <linux/of_fdt.h>
#include "../bugfix/1382_wtd_bugfix.h"
#include "hwdog_common.c"

#define SEC_TO_HZ         (HZ)
#define MIN_TO_HZ		(60 * HZ)
#define NS_PER_TICK		(NSEC_PER_SEC / HZ)
#define DEFAULT_FREQ		4
#define DEFAULT_FEED_TIME	27
#define DEFAULT_FEED_SPECAIL	100
#define REG_SIZE		4
#define RTOS_EARLY_WTD_CNT	(256 * 512)
#define PRE_NOTIFIER_CHAIN_MAX_LENGTH 10

static struct hrtimer hrtimer;
static int hrtimer_stopped;
static unsigned long long final_feed_time;
static unsigned long long feed_max;
static unsigned long long feed_count;
static unsigned long interval_sec;
static unsigned long interval_nsec;
static unsigned int interval_tick;
static unsigned int extra_time;
static unsigned int feed_special_max = DEFAULT_FEED_SPECAIL;
static unsigned int feed_special_count;
static int irq_received;
static u32 freq = DEFAULT_FREQ;
static ATOMIC_NOTIFIER_HEAD(rtos_watchdog_call_chain);
static atomic_t notifier_chain_length_count = ATOMIC_INIT(0);

unsigned long long hwdog_get_final_feed_time(void)
{
	return final_feed_time;
}
EXPORT_SYMBOL(hwdog_get_final_feed_time);

void hwdog_set_feed_count(unsigned int cnt)
{
	feed_count = cnt;
}

void hwdog_extend_feed_max(unsigned int min)
{
	feed_max += min * MIN_TO_HZ / interval_tick;
}

void hwdog_set_sec_feed_max(unsigned int sec)
{
	unsigned long long tmp;

	tmp = (unsigned long long)sec * SEC_TO_HZ;
	do_div(tmp, interval_tick);

	feed_max = tmp;
}

void hwdog_set_feed_max(unsigned int min)
{
	unsigned long long tmp;

	tmp = (unsigned long long)min * MIN_TO_HZ;
	do_div(tmp, interval_tick);

	feed_max = tmp;
}

void hwdog_reset_feed_max(void)
{
	feed_max = DEFAULT_FEED_TIME * MIN_TO_HZ / interval_tick;
}

void hwdog_set_interval(unsigned int interv)
{
	unsigned int interval = interv ? interv : 1;
	unsigned long long tmp = (unsigned long long)interval * NS_PER_TICK;

	feed_count = 0;
	feed_max *= interval_tick;
	do_div(feed_max, interval);
	interval_tick = interval;
	interval_nsec = do_div(tmp, NSEC_PER_SEC);
	interval_sec = (unsigned long)tmp;
}

static int feed_hwdog_default(void)
{
	hwdog_feed();
	return 0;
}

void hwdog_feed_in_pfnwalk(unsigned long start_pfn, unsigned long pfn)
{
	/*
	 * There can be many holes when scanning pfn,
	 * it will cost time exceed watchdog period.
	 *
	 * MUST add point for feed dog here every 512MB.
	 */
	if (!((pfn - start_pfn) % RTOS_EARLY_WTD_CNT))
		hwdog_feed();
}

static feed_handle_t feed_hwdog = feed_hwdog_default;

int register_hwdog_handle(feed_handle_t handle)
{
	if (!handle) {
		pr_err("the hwdog_handle is Null.\n");
		return -EINVAL;
	}

	if (feed_hwdog) {
		pr_err("hwdog_handle exist, if you want to register another,please unregister the current handle first\n");
		return -EEXIST;
	}

	feed_hwdog = handle;
	return 0;
}
EXPORT_SYMBOL(register_hwdog_handle);

int unregister_hwdog_handle(void)
{
	if (!feed_hwdog) {
		pr_err("no register hwdog before unregiter.\n");
		return -ENODEV;
	}

	rtos_gpio_lock();
	feed_hwdog = NULL;
	rtos_gpio_unlock();
	return 0;
}
EXPORT_SYMBOL(unregister_hwdog_handle);

static inline int hwdog_hungry(void)
{
	return receive_fiq() || hi1382_in_fiq() || irq_received;
}

void call_hwdog_handle_nolock(void)
{
	int ret;

	if (hwdog_hungry())
		return;

	if (feed_hwdog) {
		ret = feed_hwdog();
		if (!ret) {
			final_feed_time = sched_clock();
		} else {
			pr_err(DTS_WDT_ERR
				"feed_hwdog() failed, return=%d\n", ret);
		}
	}
}
EXPORT_SYMBOL(call_hwdog_handle_nolock);

void call_hwdog_handle(void)
{
	feed_special_count = 0;
	rtos_gpio_lock();
	call_hwdog_handle_nolock();
	rtos_gpio_unlock();
}
EXPORT_SYMBOL(call_hwdog_handle);

void call_hwdog_handle_special(void)
{
	if (feed_special_count++ < feed_special_max) {
		rtos_gpio_lock();
		call_hwdog_handle_nolock();
		rtos_gpio_unlock();
	}
}

static irqreturn_t hwdog_handler(int irq, void *ptr)
{
	irq_received = 1;
	call_hwdog_notify();

	return IRQ_HANDLED;
}

static void notifier_call(bool *notify_sent, unsigned long long time_section_start,
	unsigned long long time_section_end, unsigned int *pre_notify_count)
{
	unsigned long long notify_time;

	if (*notify_sent || feed_count < time_section_start || feed_count > time_section_end)
		return;

	notify_time = div_u64(feed_count * interval_tick, HZ);
	if (atomic_notifier_call_chain(
			&rtos_watchdog_call_chain, *pre_notify_count, &notify_time) != 0) {
		pr_err("rtos_watchdog_call_chain invoke failed.\n");
	} else {
		(*pre_notify_count)++;
		*notify_sent = true;
	}
}

static enum hrtimer_restart hrtimer_handler(struct hrtimer *ht)
{
	ktime_t now, interval;
	static int called_notify;
	static unsigned int pre_notify_count;
	static bool pre_notify_one_sent;
	static bool pre_notify_two_sent;
	unsigned long long temp;

	if (feed_count < feed_max) {
		call_hwdog_handle();
		feed_count++;
	} else if (!called_notify) {
		pr_info("feed-dog-cnt %llu exceed budget %llu!\n", feed_count, feed_max);
		called_notify = 1;
		feed_max += extra_time * HZ / interval_tick;
		call_softdog_notify();
	}

	temp = div_u64(feed_max, 5) * 4;
	notifier_call(&pre_notify_one_sent, div_u64(feed_max, 2), temp, &pre_notify_count);
	notifier_call(&pre_notify_two_sent, temp, feed_max, &pre_notify_count);

	now = hrtimer_cb_get_time(ht);
	interval = ktime_set(interval_sec, interval_nsec);
	hrtimer_forward(ht, now, interval);
	return HRTIMER_RESTART;
}

static void hwdog_start_hrtimer(void)
{
	ktime_t interval;

	hrtimer_init(&hrtimer, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
	hrtimer.function = hrtimer_handler;

	interval = ktime_set(interval_sec, interval_nsec);
	hrtimer_start(&hrtimer, interval, HRTIMER_MODE_REL);
}

void hwdog_stop(void)
{
	hrtimer_cancel(&hrtimer);
	hrtimer_stopped = 1;
}

static void hwdog_remap(void)
{
	drol_unmap_op(op_arr, op_nr, DROL_EARLY_IOUNMAP);
	if (drol_map_op(op_arr, op_nr, DROL_IOREMAP, DROL_IOUNMAP)) {
		pr_err("hwdog hwdog_late_map failed\n");
		op_nr = 0;
	}
}

void __init hwdog_early_init(void)
{
	if (!acpi_enable())
#ifdef CONFIG_ARM
		hwdog_simple_init(initial_boot_params, true, DROL_EARLY_IOREMAP, DROL_EARLY_IOUNMAP);
#else
		hwdog_simple_init(NULL, false, DROL_EARLY_IOREMAP, DROL_EARLY_IOUNMAP);
#endif
}

static int restart_hrtimer(struct notifier_block *nb,
			unsigned long event, void *unused)
{
	if (hrtimer_stopped)
		hwdog_start_hrtimer();

	return NOTIFY_OK;
}

static struct notifier_block restart_hrtimer_notifier = {
	.notifier_call = restart_hrtimer,
};


void __init hwdog_late_init(void)
{
	struct device_node *node = NULL;
	unsigned int time = DEFAULT_FEED_TIME;
	int irq;
	int ret;

	if (acpi_enable())
		goto no_dtb;

	hwdog_remap();
	node = of_find_compatible_node(NULL, NULL, "rtos-hwdog-irq");
	if (node) {
		irq = irq_of_parse_and_map(node, 0);
		if (irq >= 0) {
			ret = request_irq(irq, hwdog_handler, IRQF_ONESHOT, "hwdog-irq", NULL);
			if (ret)
				pr_err("request hwdog_irq %d failed\n", irq);
		}
	}

	node = of_find_compatible_node(NULL, NULL, "rtos-wtd-config");
	if (!node)
		node = of_find_compatible_node(NULL, NULL, "arm,cortex-a9-wdt");
	if (!node)
		node = of_find_compatible_node(NULL, NULL, "arm,arm64-wdt");
	if (node) {
		of_property_read_u32(node, "freq", &freq);
		if (freq == 0) {
			freq = DEFAULT_FREQ;
			pr_err("hwdog freq should not be set to 0 in device tree, use default value instead\n");
		}

		of_property_read_u32(node, "max-special-feed-count", &feed_special_max);
		of_property_read_u32(node, "kernel-feed-time", &time);
		if (time == 0) {
			time = DEFAULT_FEED_TIME;
			pr_err("kernel-feed-time should not be set to 0 in device tree, use default value instead\n");
		}
		of_property_read_u32(node, "extra-feed-time", &extra_time);
	} else {
		pr_info("no rtos-wtd-config node, use default config\n");
	}
no_dtb:
	/* feed_max will set to 0 when interval fist init */
	hwdog_set_interval(HZ / freq);
	/* reset feed_max after interval fist init */
	hwdog_set_feed_max(time);
	hwdog_start_hrtimer();
	if (register_reboot_notifier(&restart_hrtimer_notifier))
		pr_warn("can't register restart hrtimer handle\n");
}

int get_softdog_feed_stat(unsigned long long *timeout_feed, unsigned long long *cur_feed)
{
	if (!timeout_feed || !cur_feed)
		return -EINVAL;

	if (hrtimer_stopped == 1) {
		pr_err("HWDOG hrtimer already stopped\n");
		return -ENOENT;
	}
	*timeout_feed = div_u64(feed_max, freq);
	*cur_feed = div_u64(feed_count, freq);
	return 0;
}
EXPORT_SYMBOL_NS(get_softdog_feed_stat, HW_RTOS_NS);

int rtos_softdog_exceed_pre_notifier_register(struct notifier_block *nb)
{
	int ret;
	if (!nb || !nb->notifier_call) {
		pr_err("register softdog exceed pre notifier failed\n");
		return -EINVAL;
	}
	if (atomic_read(&notifier_chain_length_count) >= PRE_NOTIFIER_CHAIN_MAX_LENGTH) {
		pr_err("notifier count over max register limit:%d", PRE_NOTIFIER_CHAIN_MAX_LENGTH);
		return -EINVAL;
	}
	ret = atomic_notifier_chain_register(&rtos_watchdog_call_chain, nb);
	if (!ret)
		atomic_inc(&notifier_chain_length_count);
	return ret;
}
EXPORT_SYMBOL_NS(rtos_softdog_exceed_pre_notifier_register, HW_RTOS_NS);

int rtos_softdog_exceed_pre_notifier_unregister(struct notifier_block *nb)
{
	int ret;
	if (!nb || !nb->notifier_call) {
		pr_err("unregister softdog exceed pre notifier failed\n");
		return -EINVAL;
	}
	ret = atomic_notifier_chain_unregister(&rtos_watchdog_call_chain, nb);
	if (!ret)
		atomic_dec_if_positive(&notifier_chain_length_count);
	return ret;
}
EXPORT_SYMBOL_NS(rtos_softdog_exceed_pre_notifier_unregister, HW_RTOS_NS);
